package com.komon.parser;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;

import com.komon.annotation.SideEffect;
import com.komon.annotation.Stateless;
import com.komon.constant.Configuration;
import com.komon.constant.HttpHeader;
import com.komon.constant.HttpMethod;
import com.komon.constant.header.EntityHeader;
import com.komon.constant.header.GeneralHeader;
import com.komon.exception.RequestFormatErroException;
import com.komon.exception.StatusCodeExceptoin;
import com.komon.object.Entry;
import com.komon.object.OneRequestContext;
import com.komon.object.request.RequestLine;
import com.komon.object.request.SimpleHttpRequest;
import com.komon.util.CommonUtils;

@Stateless
public class HttpRequestParser {
	
	public SimpleHttpRequest parseNextRequest(@SideEffect InputStream socketInputStream, OneRequestContext context) throws IOException, RequestFormatErroException, StatusCodeExceptoin {
		InternalReader reader = new InternalReader(socketInputStream);
		RequestLine requestLine = parseRequestLine(reader, context);
		Map<String, String> headers = parseHeaders(reader, context);
		
		return new SimpleHttpRequest(requestLine, headers, socketInputStream);
	}
	
	private RequestLine parseRequestLine (InternalReader reader, OneRequestContext context) throws IOException, RequestFormatErroException, StatusCodeExceptoin {
		//ignore the empty line before request-line
		String currentLine = reader.skipToNextNoneEmptyCRLFLine();
		
		currentLine = CommonUtils.replaceLWSwithSingelSP(currentLine);
		int firstSpIndex = currentLine.indexOf(" ");
		try{
			String methodInRequest = currentLine.substring(0, firstSpIndex);
			HttpMethod httpMethod = CommonUtils.getEnumByString(methodInRequest, HttpMethod.class);
			if (httpMethod == null){
				//501 (Not Implemented) if the method is unrecognized or not implemented by the origin server
				throw new StatusCodeExceptoin(501);
			}
			context.setMethod(httpMethod);
			int secondSpIndex = currentLine.indexOf(" ", firstSpIndex + 1);
			// check length of uri
			if (secondSpIndex - firstSpIndex > Configuration.URL_MAX_LENGTH) {
				//A server SHOULD return 414 (Request-URI Too Long) status if a URI is longer than the server can handle 
				throw new StatusCodeExceptoin(414);
			}
			String uriInRequest = currentLine.substring(firstSpIndex + 1, secondSpIndex);
			//Note that the absolute path cannot be empty; if none is present in the original URI, it MUST be given as "/" 
			if (CommonUtils.isEmptyString(uriInRequest)) {
				uriInRequest = "/";
			} 
			//FIXME: support absoluate URI, need to decode
			// To allow for transition to absoluteURIs in all requests in future
			// versions of HTTP, all HTTP/1.1 servers MUST accept the
			// absoluteURI form in requests, even though HTTP/1.1 clients will
			// only generate them in requests to proxies.
			URI uri = new URI(uriInRequest);
			//this method will decode the uri for us
			context.setQueryString(uri.getQuery());
			//TODO verify version
			String version = currentLine.substring(secondSpIndex + 1);
			return new RequestLine(httpMethod, uri, version);
		}
		//if any exception occured during substring
		catch (IndexOutOfBoundsException e) {
			throw new RequestFormatErroException();
		} catch (URISyntaxException e) {
			//TODO
			throw new RequestFormatErroException();
		}
	}
	
	/**
	 *               *(message-header CRLF)

	 * read the raw data from socket, parse it and trim some LWS,after that, put into a map
	 * @param reader
	 * @return  The Map that contain all plain text of header, these headers have been trim 
	 * @throws IOException
	 * @throws RequestFormatErroException
	 */
	private Map<String, String> readHttpHeaders(InternalReader reader) throws IOException, RequestFormatErroException {
		//next line to the requestLine
		String currentLine = reader.readCRLFLine().trim();
		StringBuffer sb = new StringBuffer();
		//headers
		Map<String, String> headers = new HashMap<String, String>();
		
		while (!CommonUtils.isEmptyString(currentLine)) {
			//LWS, a header can be extended over multi-lines by preceding each extra line with at least one SP or HT
			if (currentLine.startsWith(" ") || currentLine.startsWith("\t")) {
				currentLine.replaceAll("( |\t)*", " ");
				sb.append(currentLine);
				continue;
			}
			else{
				Entry<String,String> headerEntry = processParseToHeader(sb.toString());
				headers.put(headerEntry.getKey(), headerEntry.getValue());
				sb = new StringBuffer();
				sb.append(currentLine);
			}
			
			currentLine = reader.readCRLFLine();
		}
		//add last time
		Entry<String,String> headerEntry = processParseToHeader(sb.toString());
		headers.put(headerEntry.getKey(), headerEntry.getValue());
		
		return headers;
	}
	
	/**
	 * top layer method of parse the http header
	 * @param reader
	 * @param context
	 * @return
	 * @throws IOException
	 * @throws RequestFormatErroException
	 */
	private  Map<String, String> parseHeaders(InternalReader reader, OneRequestContext context) throws IOException, RequestFormatErroException {
		return processHeaders(readHttpHeaders(reader), context);
	}
	

	
	/**
	 * replace the LWS with SP in a stringBuffer
	 * and put it into a Entry object
	 * @param currentHeader
	 * @return
	 * @throws RequestFormatErroException
	 */
	private Entry<String,String> processParseToHeader(String currentHeader) throws RequestFormatErroException {
		// replace the LWS with SP
		String plainText = CommonUtils.replaceLWSwithSingelSP(currentHeader);
		if (plainText.contains(":")) {
			int index = plainText.indexOf(':');
			String key = plainText.substring(0, index);
			String value = plainText.substring(index + 1, plainText.length());
			return new Entry<String, String>(key,value);
		} else {
			throw new RequestFormatErroException();
		}
	}
	
	/**
	 * 1.combine the headers with same name
	 * 2.populate the general header into context
	 * Field names are case-insensitive
	 * @param headers
	 */
	private Map<String, String> processHeaders(Map<String, String> headers, OneRequestContext context) {
		Map<String, String> tempMap = new HashMap<String, String>();
		for (Map.Entry<String, String> entry : headers.entrySet()) {
			if (tempMap.keySet().contains(entry.getKey())) {
				tempMap.put(entry.getKey(), tempMap.get(entry.getKey()) + "," + entry.getValue());
			}else {
				tempMap.put(entry.getKey(), entry.getValue());
			}
		}
		headers.clear();
		headers.putAll(tempMap);
		tempMap = null;
		//all the headers have been combined 
		
		return headers;
	}
}
